fix(macos): recover capture after the display sleeps - #5510
Conversation
Capture waited on the session semaphore with DISPATCH_TIME_FOREVER, but that semaphore is only signalled when the frame callback returns false — that is, when capture is deliberately stopped. A sleeping display stops AVCaptureSession delivering sample buffers and the session does not resume when the display wakes, so the capture thread parked for the lifetime of the process. Since capture() could also only ever return capture_e::ok, and video.cpp rebuilds the display only on capture_e::reinit, nothing could bring the stream back and the user had to restart Sunshine. Poll the semaphore instead and watch the display while waiting. A display that slept and then woke returns capture_e::reinit so the caller rebuilds it. Waiting for the wake transition rather than for a frame-arrival timeout keeps a static screen from being mistaken for a stalled one. Abandoning a capture needs the same teardown the frame callback performs when it returns false, so AVVideo grows a stopCapture: for it. Merely stopping the session leaves the output in the capture session and in the map tables, and -[AVVideo dealloc] releases those map tables before it stops the session, so the output is released while the session still holds it and the process dies in objc_msgSend during the next reinitialization. dummy_img() had the same unbounded wait, where the semaphore genuinely does mean "one frame arrived". Encoder probing hung there indefinitely when the display was asleep, so bound it and report failure instead. Tested on macOS 26.6.1 (Apple M1 Max): with a stream running, a hot corner set to Put Display to Sleep, and the display then woken, capture is rebuilt in under 100 ms and a fresh IDR frame follows within 300 ms. Two sleep/wake cycles in a row were survived by a single uninterrupted client session.
|
Thank you for the PR submission, but it looks like you used AI to create this PR. Please read and follow our Contributing guidelines and specifically our AI Usage policy. Additionally, please update the PR to use the correct template. You can find it at https://github.com/LizardByte/.github/blob/master/.github/pull_request_template.md?plain=1 |
|
|
I am sorry, I skipped the template. I have fixed and marked AI usage as Heavy. Worth adding that the first version of this patch was wrong. It detected the wake correctly and logged the right line, then killed the process ten seconds later . The reason: Please review and let me know if further changes are needed. Thanks. |



Description
Display sleep can stall capture on macOS, and the stream never recovers.
The semaphore in
capture()isn't signaled for every frame.av_video.monly signals it when the frame callback returns false, which normally happens when the session ends. If the display goes to sleep, buffers stop arriving and the capture thread can sit there forever.Use a timed wait instead and check
CGDisplayIsAsleep. If the display goes to sleep and later wakes up, stop the current capture and returncapture_e::reinit.video.cpptreats the other return values as the end of the session, so this needs to bereinit.A frame timeout doesn't work here because
AVCaptureScreenInputis change-driven. An idle desktop may not produce frames either, so lack of frames alone doesn't tell us capture is stuck. Watching the display sleep/wake state avoids that ambiguity.dummy_img()had the same unbounded wait. There the semaphore really does mean one frame arrived, but encoder probing would hang forever if the display was already asleep at startup, so that wait is bounded now too.Also use
stopCapture:rather than callingstopRunning()directly.stopRunning()on its own leaves the output registered with the session and inAVVideo's map tables.deallocthen releases those map tables before stopping the session, so the output gets released while the session still holds it.No unit test for this one. It needs a real display going to sleep and waking up, which I can't reproduce in a test. Verified on my hardware instead: macOS 26.6.1, M1 Max, built-in display, streaming to Moonlight iOS. Capture rebuilt 86 ms and 62 ms after wake across two sleep/wake cycles, fresh IDR keyframe 264 ms after wake, one client session survived both cycles, no crash reports. I don't have a second display, so the external display path is untested.
Screenshot
Issues Fixed or Closed
Roadmap Issues
Type of Change
Checklist
AI Usage
See our AI usage policy.